#!/usr/bin/env python3

# Copyright (c) 2005-2014 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import shutil
import sqlite3
import fnmatch
import tempfile
import importlib
from datetime import datetime
from ._util import Util
from ._handy import Handy, DataError
from ._element import Element


class Library:

    def __init__(self, system_library_or_user_library=False, read_only=False, tmp_dir=None):
        self._bSysOrUser = system_library_or_user_library
        self._bReadOnly = read_only
        self._tmpDir = tmp_dir if tmp_dir is not None else tempfile.mkdtemp()

        self._dataSrcDict = {}
        if True:
            selfDir = os.path.dirname(os.path.realpath(__file__))
            for fn in os.listdir(os.path.join(selfDir, "datasrc")):
                if fn == "__init__.py" or not fn.endswith(".py"):
                    continue
                modname = fn[:-3]    # remove ".py" extension
                clazz = getattr(importlib.import_module(".datasrc.%s" % (modname), package=__package__), "DataSource")
                self._dataSrcDict[modname] = clazz(os.path.join(self._tmpDir, modname))

        self._dbConnDict = {}

    def dispose(self):
        shutil.rmtree(self._tmpDir)
        del self._dbConnDict
        del self._dataSrcDict
        del self._tmpDir
        del self._bReadOnly
        del self._bSysOrUser

    def is_sync_needed(self, etype):
        conn = self._ensureDbConn(etype)
        for dataSrcObj in Handy.dataSrcFilterByType(self._dataSrcDict, etype):
            lastUpdateTime = self._readLastUpdateTime(conn, "temp", dataSrcObj.name)
            if lastUpdateTime is None or dataSrcObj.retrieve_sync_data_last_update_time() > lastUpdateTime:
                return True
        return False

    def sync(self, etype):
        conn = self._ensureDbConn(etype)
        for dataSrcObj in Handy.dataSrcFilterByType(self._dataSrcDict, etype):
            dt = dataSrcObj.retrieve_sync_data_last_update_time(etype)
            with conn:
                lastUpdateTime = self._readLastUpdateTime(conn, "temp", dataSrcObj.name)
                if lastUpdateTime is None or dt > lastUpdateTime:
                    dataSrcObj.update_sync_data(etype, conn)
                    self._writeLastUpdateTime(conn, dataSrcObj.name, dt)

    def normalize_element_id(self, etype, esource, eindex):
        return Handy.elemGetMaxPriorityElementId(self._dataSrcDict, etype, esource, eindex)

    def has_element(self, etype, esource, eindex):
        conn = self._ensureDbConn(etype)
        esource, eindex = Handy.elemGetMaxPriorityElementId(self._dataSrcDict, etype, esource, eindex)
        elementIdList = Handy.elemGetAllElementIdList(self._dataSrcDict, etype, esource, eindex)
        return self._hasAnyElement(conn, elementIdList)

    def get_element(self, etype, esource, eindex):
        conn = self._ensureDbConn(etype)
        esource, eindex = Handy.elemGetMaxPriorityElementId(self._dataSrcDict, etype, esource, eindex)
        elementIdList = Handy.elemGetAllElementIdList(self._dataSrcDict, etype, esource, eindex)
        assert self._hasAnyElement(conn, elementIdList)
        return Element(self, etype, esource, eindex, elementIdList)

    def get_elements(self, etype, esource, eindex_wildcard):
        conn = self._ensureDbConn(etype)

        elemIdSet = set()
        if True:
            c = conn.execute('''
                SELECT DISTINCT p.esource, p.eindex
                    FROM temp.element_part AS p
                    JOIN temp.datasrc AS d ON p.esource = d.name AND p.db = d.db
            ''')
            row = c.fetchone()
            while row is not None:
                if (esource == "*" or row['esource'] == esource) and fnmatch.fnmatch(row['eindex'], eindex_wildcard):
                    esource, eindex = Handy.elemGetMaxPriorityElementId(self._dataSrcDict, etype, row['esource'], row['eindex'])
                    elemIdSet.add((esource, eindex))
                row = c.fetchone()
            assert len(elemIdSet) > 0

        ret = []
        for esource, eindex in elemIdSet:
            elementIdList = Handy.elemGetAllElementIdList(self._dataSrcDict, etype, esource, eindex)
            ret.append(Element(self, etype, esource, eindex, elementIdList))
        return ret

    def clean(self, etype, all_cache=False):
        # delete metadata in user directory that is older than in system directory
        if not self._bSysOrUser:
            conn = self._ensureDbConn(etype, bNoCreate=True)
            if conn is not None:
                with conn:
                    for dataSrcObj in Handy.dataSrcFilterByType(self._dataSrcDict, etype):
                        sysLastUpdateTime = self._readLastUpdateTime(conn, "sys", dataSrcObj.name)
                        mainLastUpdateTime = self._readLastUpdateTime(conn, "main", dataSrcObj.name)
                        if sysLastUpdateTime is not None and mainLastUpdateTime is not None and mainLastUpdateTime <= sysLastUpdateTime:
                            conn.execute("DELETE FROM main.element_part WHERE esource = ?", (dataSrcObj.name, ))
                            conn.execute("DELETE FROM main.datasrc WHERE name = ?", (dataSrcObj.name, ))

        # delete redundant files for each data source, or delete all files for each data source
        for dataSrcObj in Handy.dataSrcFilterByType(self._dataSrcDict, etype):
            dataSrcObj.clean(etype, all_cache=all_cache)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.dispose()

    def _ensureDbConn(self, etype, bNoCreate=False):
        if etype in self._dbConnDict:
            return self._dbConnDict[etype]

        # current database
        conn = None
        while True:
            baseDir = Handy.SYSTEM_DIR if self._bSysOrUser else Handy.USER_DIR
            if not os.path.exists(baseDir):
                if bNoCreate:
                    return None
                os.makedirs(baseDir)
            # FIXME: should ensure mode
            pass

            fullfn = os.path.join(baseDir, "%s.metadata" % (etype))
            if os.path.exists(fullfn):
                conn = sqlite3.connect(fullfn)
                if Util.sqliteCheckTables(conn, self._getCreateTableSql()):
                    break
                else:
                    conn.close()
                    if self._bReadOnly:
                        raise DataError("invalid database file \"%s\"" % (fullfn))
                    Util.forceDelete(fullfn)
                    if bNoCreate:
                        return None
            else:
                if bNoCreate:
                    return None

            conn = sqlite3.connect("file:%s%s" % (fullfn, "?mode=ro" if self._bReadOnly else ""), uri=True)
            conn.executescript(self._getCreateTableSql())
            break

        # parent database and union-view
        # SELECT should happen in temp.*
        while True:
            s = " UNION ALL ".join(["SELECT '%s' AS datasrc, %d AS priority" % (x.name, x.get_priority(etype)) for x in Handy.dataSrcFilterByType(self._dataSrcDict, etype)])
            if s == "":
                s = "SELECT NULL AS datasrc, NULL AS priority WHERE 0"

            if not self._bSysOrUser:
                fullfn = os.path.join(Handy.SYSTEM_DIR, "%s.metadata" % (etype))
                if os.path.exists(fullfn):
                    conn.execute("ATTACH DATABASE '%s' AS sys" % (fullfn))
                    conn.execute('''
                        WITH
                            _valid_datasrc_key (
                                SELECT name, db, MAX(last_update_time)
                                    FROM (
                                        SELECT *, 'sys' AS db FROM sys.datasrc
                                        UNION ALL
                                        SELECT *, 'main' AS db FROM main.datasrc
                                    )
                                    GROUP BY name, db
                            ),
                            _datasrc_priority (
                                {s}
                            )
                        CREATE TEMP VIEW datasrc AS
                            SELECT *
                                FROM (
                                    SELECT sys.datasrc.*, priority, 'sys' AS db FROM sys.datasrc, _datasrc_priority WHERE sys.datasrc.name = _datasrc_priority.datasrc
                                    UNION ALL
                                    SELECT main.datasrc.*, priority, 'main' AS db FROM main.datasrc, _datasrc_priority WHERE main.datasrc.name = _datasrc_priority.datasrc
                                )
                                WHERE (name, db, last_update_time) IN _valid_datasrc_key
                    ''')
                    conn.execute('''
                        CREATE TEMP VIEW element_part AS
                            SELECT *, 'sys' AS db FROM sys.element_part
                            UNION ALL
                            SELECT *, 'main' AS db FROM main.element_part
                    ''')
                    break

            conn.executescript(f'''
                WITH
                    _valid_datasrc_key (
                        SELECT name, 'sys' AS db, MAX(last_update_time)
                            FROM sys.datasrc
                            GROUP BY name, db
                    ),
                    _datasrc_priority (
                        {s}
                    )
                CREATE TEMP VIEW datasrc AS
                    SELECT sys.datasrc.*, priority, 'sys' AS db
                        FROM sys.datasrc, _datasrc_priority
                        WHERE sys.datasrc.name = _datasrc_priority.datasrc
                            AND (name, db, last_update_time) IN _valid_datasrc_key
            ''')
            conn.execute('''
                CREATE TEMP VIEW element_part AS
                    SELECT *, 'sys' AS db FROM sys.element_part
            ''')
            break

        self._dbConnDict[etype] = conn
        return conn

    @staticmethod
    def _hasAnyElement(conn, elementIdList):
        s = " UNION ALL ".join([f"SELECT '{x}' AS esource, '{y}' AS eindex" for x, y in elementIdList])
        c = conn.execute(f'''
            WITH
                _filter AS (
                    {s}
                )
            SELECT DISTINCT p.esource, p.eindex
                FROM temp.element_part AS p
                JOIN temp.datasrc AS d ON p.esource = d.name AND p.db = d.db
                JOIN _filter AS f ON p.esource = f.esource AND p.eindex = f.eindex
        ''')
        if c.fetchone() is not None:
            return True
        return False

    @staticmethod
    def _readLastUpdateTime(conn, schema, dataSrcName):
        c = conn.execute(f"SELECT MAX(last_update_time) FROM {schema}.datasrc WHERE name = ?", (dataSrcName, ))
        row = c.fetchone()
        if row is not None:
            return datetime.fromtimestamp(row[0])
        else:
            return None

    @staticmethod
    def _writeLastUpdateTime(conn, dataSrcName, dt):
        conn.execute('''
            INSERT OR REPLACE INTO main.datasrc
                (name, last_update_time)
                VALUES (?, ?)
        ''', (dataSrcName, int(dt.timestamp()), ))

    @staticmethod
    def _getCreateTableSql():
        return '''
            CREATE TABLE element_part (
                esource TEXT NOT NULL,
                eindex TEXT NOT NULL,
                pname TEXT NOT NULL,
                private_data BLOB,
                PRIMARY KEY (esource, eindex, pname)
            );
            CREATE TABLE datasrc (
                name TEXT,
                last_update_time INTEGER NOT NULL,      -- as unix epoch, unit: second
                PRIMARY KEY (name)
            );
        '''
